﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using AliyunOpenSearch4Net.App;
using AliyunOpenSearch4Net.Base;
using AliyunOpenSearch4Net.Data;
using AliyunOpenSearch4Net.Error;
using AliyunOpenSearch4Net.Index;
using AliyunOpenSearch4Net.Search;
using AliyunOpenSearch4Net.Suggest;

namespace AliyunOpenSearch4Net
{
    public partial class AliyunClient
    {
        class AsyncTaskState<TResponse>
        {
            public TaskCompletionSource<TResponse> TaskSrc { get; set; }
            public WebRequest Request { get; set; }
            public object Data { get; set; }
        }

        private void OnGetRequestStreamEnd<TResponse>(IAsyncResult result)
        {
            var state = result.AsyncState as AsyncTaskState<WebRequest>;

            try
            {
                var stream = state.Request.EndGetRequestStream(result);
                WriteRequestBody(stream, state.Data as string);
                state.Request.BeginGetResponse(OnGetResponseEnd<TResponse>, result.AsyncState);
            }
            catch (Exception e)
            {
                state.TaskSrc.SetException(e);
                return;
            }
        }

        internal Task<TResponse> GetResponseAsync<TRequest, TResponse>(TRequest request, string path, string method)
            where TRequest : RequestBase<TResponse>
            where TResponse : Response, new()

        {
            var tcs = new TaskCompletionSource<TResponse>();

            var body = string.Empty;

            var httpRequest = CreatePreparedWebRequest(request, path, method, out body);

            var asyncState = new AsyncTaskState<TResponse>
            {
                TaskSrc = tcs,
                Request = httpRequest
            };

            if (httpRequest.Method == HttpMethod.POST) // if the method is POST, write request body
            {
                httpRequest.ContentType = "application/x-www-form-urlencoded";
                asyncState.Data = body;
                httpRequest.BeginGetRequestStream(OnGetRequestStreamEnd<TResponse>, asyncState);
            }
            else
            {
                httpRequest.BeginGetResponse(OnGetResponseEnd<TResponse>, asyncState);
            }

            return tcs.Task;
        }

        private void OnGetResponseEnd<TResponse>(IAsyncResult result)
        {
            var state = result.AsyncState as AsyncTaskState<TResponse>;

            WebResponse response;

            try
            {
                response = state.Request.EndGetResponse(result);

                using (var stream = response.GetResponseStream())
                using (var reader = new StreamReader(stream, Encoding.UTF8, false))
                {
                    var data = reader.ReadToEnd();
                    state.TaskSrc.SetResult(DeserializeObject<TResponse>(data));
                };
            }
            catch (Exception e)
            {
                state.TaskSrc.SetException(e);
                return;
            }
        }

        #region app operation

        /// <summary>
        /// Gets the application status.
        /// </summary>
        /// <param name="appName">Name of the application.</param>
        /// <returns></returns>
        public Task<Response<AppStatusResult>> GetAppStatusAsync(string appName)
        {
            var request = new AppStatusRequest
            {
                AppName = appName
            };

            return GetResponseAsync<AppStatusRequest, Response<AppStatusResult>>(request, "/index/" + appName, HttpMethod.GET);
        }

        /// <summary>
        /// Deletes the application.
        /// </summary>
        /// <param name="appName">Name of the application.</param>
        /// <returns></returns>
        public Task<Response<int>> DeleteAppAsync(string appName)
        {
            var request = new AppDeleteRequest
            {
                AppName = appName
            };

            return GetResponseAsync<AppDeleteRequest, Response<int>>(request, "/index/" + appName, HttpMethod.POST);
        }


        /// <summary>
        /// Creates the application.
        /// </summary>
        /// <param name="appName">Name of the application.</param>
        /// <param name="template">The template.</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentNullException">
        /// appName
        /// or
        /// template
        /// </exception>
        public Task<Response<AppCreateResult>> CreateAppAsync(string appName, string template)
        {
            if (string.IsNullOrEmpty(appName))
                throw new ArgumentNullException("appName");

            if (string.IsNullOrEmpty(template))
                throw new ArgumentNullException("template");

            var request = new AppCreateRequest
            {
                AppName = appName,
                Template = template
            };

            return GetResponseAsync<AppCreateRequest, Response<AppCreateResult>>(request, "/index/" + appName, HttpMethod.POST);
        }

        /// <summary>
        /// Get the list of all applications.
        /// </summary>
        /// <param name="page">page index.</param>
        /// <param name="pageSize">Size of the page.</param>
        /// <returns></returns>
        public Task<AppListResponse> ListAppAsync(int page, int pageSize)
        {
            var request = new AppListRequest
            {
                Page = page,
                PageSize = pageSize
            };

            return GetResponseAsync<AppListRequest, AppListResponse>(request, "/index", HttpMethod.GET);
        }

        #endregion

        #region data operation

        /// <summary>
        /// Uploads the data.
        /// </summary>
        /// <param name="appName">Name of the application.</param>
        /// <param name="tableName">Name of the table.</param>
        /// <param name="items">The data push items.</param>
        /// <returns></returns>
        public Task<Response> UploadDataAsync(string appName, string tableName, IList<DataItem> items)
        {
            var request = new DataPushRequest
            {
                TableName = tableName,
                Items = items
            };

            return GetResponseAsync<DataPushRequest, Response>(request, "/index/doc/" + appName, HttpMethod.POST);
        }

        #endregion

        #region error

        /// <summary>
        /// Gets the errors list.
        /// </summary>
        /// <param name="appName">Name of the application.</param>
        /// <param name="page">The page index.</param>
        /// <param name="pageSize">Size of the page.</param>
        /// <param name="sort">The sort.</param>
        /// <returns></returns>
        public Task<Response<ErrorListResult>> GetErrorsAsync(string appName, int page, int pageSize, SortMode sort)
        {
            var request = new ErrorListRequest
            {
                Page = page,
                PageSize = pageSize,
                SortMode = sort
            };

            return GetResponseAsync<ErrorListRequest, Response<ErrorListResult>>(request, "/index/error/" + appName, HttpMethod.GET);
        }

        #endregion

        #region suggestions

        /// <summary>
        /// Gets the suggestions.
        /// </summary>
        /// <param name="appName">Name of the application.</param>
        /// <param name="query">The query used for getting the suggestions.</param>
        /// <param name="suggestName">Name of the suggest.</param>
        /// <param name="hint">The hint, how many suggestions you want to get.</param>
        /// <returns></returns>
        public Task<SuggestResponse> GetSuggestionsAsync(string appName, string query, string suggestName, int hint)
        {
            var request = new SuggestRequest
            {
                IndexName = appName,
                Query = query,
                SuggestName = suggestName,
                Hint = hint
            };

            return GetResponseAsync<SuggestRequest, SuggestResponse>(request, "/suggest", HttpMethod.GET);
        }

        #endregion

        #region index

        /// <summary>
        /// Rebuilds the index.
        /// </summary>
        /// <param name="appName">Name of the application.</param>
        /// <param name="tableName">Name of the table.</param>
        /// <param name="operate">The operate.</param>
        /// <returns></returns>
        public Task<Response> RebuildIndexAsync(string appName, string tableName, string operate)
        {
            var request = new IndexRebuildRequest
            {
                TableName = tableName,
                Operate = operate
            };

            return GetResponseAsync<IndexRebuildRequest, Response>(request, "/index/" + appName, HttpMethod.GET);
        }

        #endregion

        #region search

        /// <summary>
        /// Searches within the specific application.
        /// </summary>
        /// <param name="appName">Name of the application.</param>
        /// <param name="query">The query used for searching.</param>
        /// <param name="fetchFields">The fetch fields.</param>
        /// <param name="disabled">if set to <c>true</c> [disabled].</param>
        /// <param name="firstFormulaName">First name of the formula.</param>
        /// <param name="formulaName">Name of the formula.</param>
        /// <param name="qp">The qp.</param>
        /// <param name="summary">The summary.</param>
        /// <returns></returns>
        public Task<Response<SearchResult>> SearchAsync(string appName, string query, string fetchFields = null, bool disabled = false, string firstFormulaName = null, string formulaName = null, string qp = null, SearchSummary summary = null)
        {
            var request = new SearchRequest
            {
                IndexName = appName,
                Query = query,
                FetchFields = fetchFields,
                Disable = disabled,
                FirstFormulaName = firstFormulaName,
                FormulaName = formulaName,
                QP = qp,
                Summary = summary
            };

            return GetResponseAsync<SearchRequest, Response<SearchResult>>(request, "/search", HttpMethod.GET);
        }

        /// <summary>
        /// Scans within the speciic application.
        /// </summary>
        /// <param name="appName">Name of the application.</param>
        /// <param name="query">The query.</param>
        /// <param name="scroll">The scroll.</param>
        /// <param name="searchType">Type of the search.</param>
        /// <param name="scrollId">The scroll id.</param>
        /// <returns></returns>
        public Task<Response<ScanResult>> ScanAsync(string appName, string query, string scroll, string searchType = null, string scrollId = null)
        {
            var request = new ScanRequest
            {
                IndexName = appName,
                Query = query,
                Scroll = scroll,
                ScrollId = scrollId,
                SearchType = searchType
            };

            return GetResponseAsync<ScanRequest, Response<ScanResult>>(request, "/search", HttpMethod.GET);
        }

        #endregion
    }
}
